package com.zhigh.project.security.jwt.authentication;

import com.zhigh.project.security.jwt.authentication.exception.JWTAuthenticationException;
import com.zhigh.project.security.jwt.authentication.exception.JWTDisabledException;
import com.zhigh.project.security.jwt.authentication.exception.JWTExpiredException;
import com.zhigh.project.security.jwt.authentication.exception.JWTSecretNotFoundException;
import com.zhigh.project.security.jwt.authentication.principal.AbstractJWTPrincipal;
import com.zhigh.project.tools.Assertions;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.util.Assert;

public abstract class AbstractJWTAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {

    protected final Log logger = LogFactory.getLog(getClass());

    protected boolean hideSecretNotFound = true;

    protected MessageSourceAccessor messages = JWTMessageSource.getAccessor();

    private JWTChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();

    private JWTChecker postAuthenticationChecks = new NoneChecker();

    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assertions.assertIsInstanceOf(JWTAuthentication.class, authentication,
                () -> new JWTAuthenticationException(this.messages.getMessage("AbstractJWTAuthenticationProvider.onlySupports",
                        "Only JWTAuthenticationToken is supported")));
        JWTAuthentication token = (JWTAuthentication) authentication;

        AbstractJWTPrincipal principal =
                Assertions.assertNonnull(token.getPrincipal(),
                        () -> new BadCredentialsException(
                                messages.getMessage("AbstractJWTAuthenticationProvider.authentication.principal.isnull",
                                        "Principal is null.")));

        JWT jwt = Assertions.assertNonnull(principal.getJwt(), () -> new BadCredentialsException(
                messages.getMessage("AbstractJWTAuthenticationProvider.authentication.principal.nonJwt", "Jwt is null")
        ));

        String secret;
        try {
            secret = Assertions.assertNonnull(getSecret(jwt), () -> new JWTSecretNotFoundException(
                    messages.getMessage("AbstractJWTAuthenticationProvider.authentication.secret.notFound", "Secret is null.")
            ));
        } catch (JWTSecretNotFoundException ex) {
            if (!hideSecretNotFound) throw ex;

            throw new BadCredentialsException(
                    messages.getMessage(
                            "AbstractJWTAuthenticationProvider.authentication.hide.secret.notFound",
                            "JWT is invalid."),
                    ex);
        }
        preAuthenticationChecks.check(jwt);

        additionalAuthenticate(token);

        postAuthenticationChecks.check(jwt);

        return createSuccessAuthentication(principal, secret, token);
    }

    protected void additionalAuthenticate(JWTAuthentication token) {

    }

    protected Authentication createSuccessAuthentication(AbstractJWTPrincipal principal, String secret, JWTAuthentication token) {
        JWTAuthentication result = new JWTAuthentication(principal, authoritiesMapper.mapAuthorities(token.getAuthorities()));
        result.setDetails(principal.getJwt());
        logger.debug("Authenticated JSON Web Token.");
        return result;
    }

    protected abstract String getSecret(JWT jwt);

    @Override
    public void setMessageSource(@NonNull MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return JWTAuthentication.class.isAssignableFrom(authentication);
    }

    @Override
    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.messages, "A message source must be set");
        doAfterPropertiesSet();
    }

    protected void doAfterPropertiesSet() throws Exception {
    }


    public void setPreAuthenticationChecks(JWTChecker preAuthenticationChecks) {
        this.preAuthenticationChecks = preAuthenticationChecks;
    }

    public void setPostAuthenticationChecks(JWTChecker postAuthenticationChecks) {
        this.postAuthenticationChecks = postAuthenticationChecks;
    }

    public void setHideSecretNotFound(boolean hideSecretNotFound) {
        this.hideSecretNotFound = hideSecretNotFound;
    }

    public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
        this.authoritiesMapper = authoritiesMapper;
    }

    public boolean isHideSecretNotFound() {
        return hideSecretNotFound;
    }

    public JWTChecker getPreAuthenticationChecks() {
        return preAuthenticationChecks;
    }

    public JWTChecker getPostAuthenticationChecks() {
        return postAuthenticationChecks;
    }

    public GrantedAuthoritiesMapper getAuthoritiesMapper() {
        return authoritiesMapper;
    }

    public MessageSourceAccessor getMessages() {
        return messages;
    }

    private static class NoneChecker implements JWTChecker {
        @Override
        public void check(JWT jwt) {
            // Nothing
        }
    }

    private class DefaultPreAuthenticationChecks implements JWTChecker {
        @Override
        public void check(JWT jwt) {
            if (!jwt.isEnabled()) {
                AbstractJWTAuthenticationProvider.this.logger.debug("Failed to authenticate since JSON web token is disabled.");
                throw new JWTDisabledException(AbstractJWTAuthenticationProvider.this.messages
                        .getMessage("AbstractJWTAuthenticationProvider.disabled", "JSON Web Token is disabled"));
            }

            if (!jwt.isNonExpired()) {
                AbstractJWTAuthenticationProvider.this.logger.debug("Failed to authenticate since JSON web token is expired.");
                throw new JWTExpiredException(AbstractJWTAuthenticationProvider.this.messages
                        .getMessage("AbstractJWTAuthenticationProvider.expired", "JSON Web Token is expired"));
            }
        }
    }

}
